Swadhin Biswas Feature/ai code reviewer (#79) · 8/16/2026
cfd400d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import type { APIRoute } from "astro";
import { getDatabase, schema } from "@/db";
import { eq, and, inArray } from "drizzle-orm";
import { getUserFromRequest } from "@/lib/auth";
import { unauthorized, badRequest, success, notFound, forbidden, serverError } from "@/lib/api";
import { withErrorHandler } from "@/lib/errors";
import { logger } from "@/lib/logger";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { canAdminOrg } from "@/lib/permissions";
import { generateId } from "@/lib/utils";
import { z } from "zod";
/**
* First-party team management (WS3-03).
* Teams previously existed only via SCIM — this provides CRUD + members.
*/
const createTeamSchema = z.object({
name: z.string().min(1).max(60),
description: z.string().max(300).optional(),
privacy: z.enum(["visible", "secret"]).optional().default("visible"),
memberIds: z.array(z.string()).optional(),
});
async function getOrg(db: any, org: string) {
return db.query.organizations.findFirst({
where: eq(schema.organizations.name, org),
});
}
// GET: list teams in the org (visible ones for members, all for admins)
export const GET: APIRoute = withErrorHandler(async ({ request, params }) => {
const user = await getUserFromRequest(request);
if (!user) return unauthorized();
const { org } = params;
const db = getDatabase() as NodePgDatabase<typeof schema>;
const organization = await getOrg(db, org!);
if (!organization) return notFound("Organization not found");
const isAdmin = await canAdminOrg(user.userId, organization.id);
const teams = await db.query.teams.findMany({
where: isAdmin ? eq(schema.teams.organizationId, organization.id) : eq(schema.teams.organizationId, organization.id),
with: {
members: { with: { user: true } },
},
});
return success({
teams: teams
.filter((t) => isAdmin || t.privacy !== "secret")
.map((t) => ({
id: t.id,
name: t.name,
slug: t.slug,
description: t.description,
privacy: t.privacy,
members: t.members.map((m) => ({
id: m.user.id,
username: m.user.username,
role: m.role,
})),
})),
});
});
// POST: create a team
export const POST: APIRoute = withErrorHandler(async ({ request, params }) => {
const user = await getUserFromRequest(request);
if (!user) return unauthorized();
const { org } = params;
const body = await request.json();
const parsed = createTeamSchema.safeParse(body);
if (!parsed.success) return badRequest("Invalid input", parsed.error);
const db = getDatabase() as NodePgDatabase<typeof schema>;
const organization = await getOrg(db, org!);
if (!organization) return notFound("Organization not found");
if (!(await canAdminOrg(user.userId, organization.id))) return forbidden();
// Slug uniqueness within the org
const slug = parsed.data.name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
const existing = await db.query.teams.findFirst({
where: and(
eq(schema.teams.organizationId, organization.id),
eq(schema.teams.slug, slug),
),
});
if (existing) return badRequest("A team with this name already exists");
const id = generateId("team");
await db.insert(schema.teams).values({
id,
organizationId: organization.id,
name: parsed.data.name,
slug,
description: parsed.data.description,
privacy: parsed.data.privacy,
});
if (parsed.data.memberIds?.length) {
await db.insert(schema.teamMembers).values(
parsed.data.memberIds.map((userId) => ({
teamId: id,
userId,
role: "member",
createdAt: new Date(),
})),
);
}
logger.info({ userId: user.userId, orgId: organization.id, teamId: id }, "Team created");
return success({ team: { id, name: parsed.data.name, slug } });
});